/*
 *  Copyright (c) 2011-2015 The original author or authors
 *  ------------------------------------------------------
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *       The Eclipse Public License is available at
 *       http://www.eclipse.org/legal/epl-v10.html
 *
 *       The Apache License v2.0 is available at
 *       http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */

package com.taobao.middleware.cli;

import java.util.Set;
import java.util.TreeSet;

/**
 * Models command line options. Options are values passed to a command line interface using -x or --x. Supported
 * syntaxes depend on the parser.
 * <p/>
 * Short name is generally used with a single dash, while long name requires a double-dash.
 *
 * @author Clement Escoffier <clement@apache.org>
 */
public class Option {

    /**
     * Default name in the usage message.
     */
    public static final String DEFAULT_ARG_NAME = "value";

    /**
     * The default long name / short name of the option. Notice that options requires at least a regular long name or
     * short name.
     */
    public static final String NO_NAME = "\0";

    /**
     * the option long name.
     */
    protected String longName = NO_NAME;

    /**
     * the option short name.
     */
    protected String shortName = NO_NAME;

    /**
     * the option name used in usage message.
     */
    protected String argName = DEFAULT_ARG_NAME;

    /**
     * The option description.
     */
    protected String description;

    /**
     * whether or not the option is required. A mandatory not set throws a {@link MissingOptionException}.
     */
    protected boolean required;

    /**
     * whether or not the option is hidden. Hidden options are not displayed in usage.
     */
    protected boolean hidden;

    /**
     * whether or not the option receives a single value. {@code true} by default.
     */
    protected boolean singleValued = true;

    /**
     * whether or not the option can recevie multiple values.
     */
    protected boolean multiValued;

    /**
     * the option default value.
     */
    protected String defaultValue;

    /**
     * whether or not the option is a flag. Flag option does not require a value. If an option is a flag, it is
     * evaluated to {@link true} if the option is used in the command line.
     */
    protected boolean flag;

    /**
     * whether or not the option is a "help" option. Is the user execute the command line enabling a help option, the
     * command line validation won't fail, and give the command the opportunity to display the usage message, instead
     * of throwing an exception during the parsing.
     */
    protected boolean help;

    /**
     * if the option value has to be in a definited set, this field represents the set of values. Value are sorted
     * alphabetically.
     */
    protected Set<String> choices = new TreeSet<String>();

    /**
     * Creates a new empty instance of {@link Option}.
     */
    public Option() {
    }

    /**
     * Creates a new instance of {@link Option} by copying the state of another {@link Option}.
     *
     * @param other the other option
     */
    public Option(Option other) {
        this();
        this.longName = other.longName;
        this.shortName = other.shortName;
        this.argName = other.argName;
        this.description = other.description;
        this.required = other.required;
        this.hidden = other.hidden;
        this.singleValued = other.singleValued;
        this.multiValued = other.multiValued;
        this.defaultValue = other.defaultValue;
        this.flag = other.flag;
        this.help = other.help;
        this.choices = other.choices;
    }

    /**
     * Creates a new instance of {@link Option} from the given {@link JsonObject}
     *
     * @param json the json object representing the option
     * @see #toJson()
     */
//  public Option(JsonObject json) {
//    this();
//    OptionConverter.fromJson(json, this);
//  }

    /**
     * Gets the json representation of this {@link Option}.
     *
     * @return the json representation
     */
//  public JsonObject toJson() {
//    JsonObject json = new JsonObject();
//    OptionConverter.toJson(this, json);
//    return json;
//  }

    /**
     * Checks whether or not the option is valid. This implementation check that it has a short name or a long name.
     * This method is intended to be extended by sub-class. Parser should check that the set of
     * option of a {@link CLI} is valid before starting the parsing.
     * <p/>
     * If the configuration is not valid, this method throws a {@link IllegalArgumentException}.
     */
    public void ensureValidity() {
        if ((shortName == null || shortName.equals(NO_NAME)) && (longName == null || longName.equals(NO_NAME))) {
            throw new IllegalArgumentException("An option needs at least a long name or a short name");
        }
    }

    /**
     * @return whether or not the option can receive a value.
     */
    public boolean acceptValue() {
        return singleValued || multiValued;
    }

    /**
     * @return the option name. It returns the long name if set, the short name otherwise. It cannot return {@code
     * null} for valid option
     * @see #ensureValidity()
     */
    public String getName() {
        if (longName != null && !longName.equals(NO_NAME)) {
            return longName;
        }
        // So by validity, it necessarily has a short name.
        return shortName;
    }

    /**
     * @return whether or not this option can receive several values.
     */
    public boolean isMultiValued() {
        return multiValued;
    }

    /**
     * Sets whether or not this option can receive several values.
     *
     * @param multiValued whether or not this option is multi-valued.
     * @return the current {@link Option} instance
     */
    public Option setMultiValued(boolean multiValued) {
        this.multiValued = multiValued;
        if (this.multiValued) {
            // If we accept more than one, we also accept 1.
            this.singleValued = true;
        }
        // Else we cannot say what is the desired value of singleValued, it needs to be explicitly set.
        return this;
    }

    /**
     * @return whether or not this option is single valued.
     */
    public boolean isSingleValued() {
        return singleValued;
    }

    /**
     * Sets whether or not this option can receive a value.
     *
     * @param singleValued whether or not this option is single-valued.
     * @return the current {@link Option} instance
     */
    public Option setSingleValued(boolean singleValued) {
        this.singleValued = singleValued;
        return this;
    }

    /**
     * @return the option arg name used in usage messages, {@code null} if not set.
     */
    public String getArgName() {
        return argName;
    }

    /**
     * Sets te arg name for this option.
     *
     * @param argName the arg name, must not be {@code null}
     * @return the current {@link Option} instance
     */
    public Option setArgName(String argName) {
        if (argName == null) {
            throw new NullPointerException();
        }
        this.argName = argName;
        return this;
    }

    /**
     * @return the description of this option, {@code null} if not set.
     */
    public String getDescription() {
        return description;
    }

    /**
     * Sets te description of this option.
     *
     * @param description the description
     * @return the current {@link Option} instance
     */
    public Option setDescription(String description) {
        this.description = description;
        return this;
    }

    /**
     * @return whtehr or not this option is hidden.
     */
    public boolean isHidden() {
        return hidden;
    }

    /**
     * Sets whether or not this option should be hidden
     *
     * @param hidden {@code true} to make this option hidden, {@link false} otherwise
     * @return the current {@link Option} instance
     */
    public Option setHidden(boolean hidden) {
        this.hidden = hidden;
        return this;
    }

    /**
     * @return the option long name, {@code null} if not set.
     */
    public String getLongName() {
        return longName;
    }

    /**
     * Sets the long name of this option.
     *
     * @param longName the long name
     * @return the current {@link Option} instance
     */
    public Option setLongName(String longName) {
        this.longName = longName;
        return this;
    }

    /**
     * @return whether or not this option is mandatory.
     */
    public boolean isRequired() {
        return required;
    }

    /**
     * Sets whether or not this option is mandatory.
     *
     * @param required {@code true} to make this option mandatory, {@link false} otherwise
     * @return the current {@link Option} instance
     */
    public Option setRequired(boolean required) {
        this.required = required;
        return this;
    }

    /**
     * @return the short name of this option, {@code null} if not set.
     */
    public String getShortName() {
        return shortName;
    }

    /**
     * Sets the short name of this option.
     *
     * @param shortName the short name
     * @return the current {@link Option} instance
     */
    public Option setShortName(String shortName) {
        this.shortName = shortName;
        return this;
    }

    /**
     * @return the default value of this option, {@code null} if not set.
     */
    public String getDefaultValue() {
        return defaultValue;
    }

    /**
     * Sets the default value of this option
     *
     * @param defaultValue the default value
     * @return the current {@link Option} instance
     */
    public Option setDefaultValue(String defaultValue) {
        this.defaultValue = defaultValue;
        if (this.defaultValue != null) {
            setRequired(false);
        }
        return this;
    }

    /**
     * @return whether or not this option is a flag.
     */
    public boolean isFlag() {
        return flag;
    }

    /**
     * Configures the current {@link Option} to be a flag. It will be evaluated to {@code true} if it's found in
     * the command line. If you need a flag that may receive a value, use, in this order:
     * <code><pre>
     *   option.setFlag(true).setSingleValued(true)
     * </pre></code>
     *
     * @param flag whether or not the option is a flag.
     * @return the current {@link Option}
     */
    public Option setFlag(boolean flag) {
        this.flag = flag;
        if (flag) {
            setSingleValued(false);
        }

        return this;
    }

    /**
     * Checks whether or not this option is a "Help" option.
     *
     * @return {@code true} if this option is a "help" option.
     */
    public boolean isHelp() {
        return help;
    }

    /**
     * Sets whether or not this option is a "help" option
     *
     * @param help {@code true} to set this option as a "Help" option
     * @return the current {@link Option}
     */
    public Option setHelp(boolean help) {
        this.help = help;
        return this;
    }

    /**
     * @return get the list of choices for the given option. Empty if this option does not define choices.
     */
    public Set<String> getChoices() {
        return choices;
    }

    /**
     * Sets the list of values accepted by this option. If the value set by the user does not match once of these
     * values, a {@link InvalidValueException} exception is thrown.
     *
     * @param choices the choices
     * @return the current {@link Option}
     */
    public Option setChoices(Set<String> choices) {
        this.choices = choices;
        return this;
    }

    /**
     * Adds a choice to the list of values accepted by this option. If the value set by the user does not match once of these
     * values, a {@link InvalidValueException} exception is thrown.
     *
     * @param choice the choice
     * @return the current {@link Option}
     */
    public Option addChoice(String choice) {
        this.choices.add(choice);
        return this;
    }
}
